import java.util.ArrayList;
import java.util.List;

/**
 * Provides the general functionality for nxn matrix operations.
 * 
 * @author Sean
 *
 */
public class Matrix {

	/**
	 * Stores the array as a list of row vectors.
	 */
	List<Vector> my_current_matrix;
	
	private static List<Vector> ID_MATRIX_4D;
	
	/**
	 * Default constructor makes the ID matrix for a 4x4 matrix, initializes current matrix to id.
	 */
	public Matrix() {
		ID_MATRIX_4D = buildIDMatrix();
		my_current_matrix = ID_MATRIX_4D;
	}
	
	/**
	 * Constructor creates the ID matrix for a 4x4 matrix, initializes current matrix to specified matrix.
	 * @param inList
	 */
	public Matrix(List<Vector> inList) {
		ID_MATRIX_4D = buildIDMatrix();
		my_current_matrix = new ArrayList<Vector>();
		for(Vector v : inList) {
			my_current_matrix.add(v);
		}
	}
	
	/**
	 * Multiplies a given vector by a given matrix.
	 * 
	 * @throws IllegalArgumentException if the column count does not match the vector dimension.
	 * @param a the transformation matrix.
	 * @param v the vector.
	 * @return the resulting vector.
	 */
	public static Vector multiply(Matrix a, Vector v) {
		if(a.getColumnCount() != v.getDimension()) 
			throw new IllegalArgumentException("Dimension of vector must match columns of matrix");
		ArrayList<Double> results = new ArrayList<Double>();
		for(int i = 0; i < a.getRowCount(); i++) {
			results.add(a.getRowVector(i).product(v));
		}
		return new Vector(results);
	}
	
	/**
	 * Multiplies a given vector by the current matrix.
	 * 
	 * @throws IllegalArgumentException if the column count does not match the vector dimension.
	 * @param v the vector.
	 * @return the resulting vector.
	 */
	public Vector multiply(Vector v) {
		return multiply(this, v);
	}
	
	/**
	 * Multiplies two matrices together.
	 * Invariant: AxB works only if number of columns in A is the number of rows in B.
	 * 
	 * @throws IllegalArgumentException if the matrices break the row/column invariant.
	 * @param a the first matrix.
	 * @param b the second matrix.
	 * @return a matrix with the results.
	 */
	public static Matrix multiply(Matrix a, Matrix b) {
		if(b.getRowCount() != a.getColumnCount()) throw new IllegalArgumentException("Matrices are not compatible");
		List<Vector> result = new ArrayList<Vector>();
		for(int i = 0; i < a.getRowCount(); i++) {
			ArrayList<Double> results = new ArrayList<Double>();
			Vector current_row = a.getRowVector(i);
			for(int j = 0; j < current_row.getDimension(); j++)
				results.add(current_row.product(b.getColumnVector(j)));
			result.add(new Vector(results));
		}
		return new Matrix(result);
	}

	/**
	 * Multiplies two matrices together.
	 * Invariant: AxB works only if number of columns in A is the number of rows in B.
	 * 
	 * @throws IllegalArgumentException if the matrices break the row/column invariant.
	 * @param b the matrix to multiply on the right by.
	 * @return a matrix with the results.
	 */
	public Matrix multiply(Matrix b) {
		return multiply(this, b);
	}
	
	/**
	 * Adds two matrices together.
	 * 
	 * @throws IllegalArgumentException if matrices have different dimensions.
	 * @param a The first matrix.
	 * @param b The second matrix.
	 * @return the resulting matrix.
	 */
	public static Matrix add(Matrix a, Matrix b) {
		if(b.getRowCount() != a.getRowCount() || b.getColumnCount() != a.getColumnCount())
			throw new IllegalArgumentException("The matrices must have the same dimension.");
		List<Vector> result = new ArrayList<Vector>();
		for(int i = 0; i < a.getRowCount(); i++) {
			ArrayList<Double> results = new ArrayList<Double>();
			Vector current_row = a.getRowVector(i);
			for(int j = 0; j < current_row.getDimension(); j++)
				results.add(current_row.getValue(j) + b.getRowVector(i).getValue(j));
			result.add(new Vector(results));
		}
		return new Matrix(result);	
	}
	/**
	 * Adds two matrices together.
	 * 
	 * @throws IllegalArgumentException if matrices have different dimensions.
	 * @param b The second matrix.
	 * @return the resulting matrix.
	 */
	public Matrix add(Matrix b) {
		return add(this, b);
	}
	
	/**
	 * Scales a matrix by a given value.
	 * 
	 * @param a the matrix to scale.
	 * @param s the scalar.
	 * @return the scaled matrix.
	 */
	public static Matrix scale(Matrix a, double s) {
		List<Vector> result = new ArrayList<Vector>();
		for(int i = 0; i < a.getRowCount(); i++) {
			ArrayList<Double> results = new ArrayList<Double>();
			Vector current_row = a.getRowVector(i);
			for(int j = 0; j < current_row.getDimension(); j++)
				results.add(current_row.getValue(j) * s);
			result.add(new Vector(results));
		}
		return new Matrix(result);	
	}
	
	/**
	 * Scales this matrix by a given value.
	 * 
	 * @param s the scalar.
	 * @return the scaled matrix.
	 */
	public Matrix scale(double s) {
		return scale(this, s);
	}
	
	/**
	 * Transposes a matrix.
	 * 
	 * @param a the matrix to transpose.
	 * @return the transposed matrix.
	 */
	public static Matrix transpose(Matrix a) {
		ArrayList<Vector> results = new ArrayList<Vector>();
		int oldColumns = a.getColumnCount();
		int oldRows = a.getRowCount();
		for(int i = 0; i < oldColumns; i++) {
			ArrayList<Double> newRow = new ArrayList<Double>();
			for(int j = 0; j < oldRows; j++) {
				newRow.add(a.getRowVector(j).getValue(i));
			}
			results.add(new Vector(newRow));
		}
		
		return new Matrix(results);
	}
	
	/**
	 * Transposes this matrix.
	 * 
	 * @return the transposition of this matrix.
	 */
	public Matrix transpose() {
		return transpose(this);
	}
	
	/**
	 * @return the number of columns in this matrix.
	 */
	private int getColumnCount() {
		return this.my_current_matrix.get(0).getDimension();
	}
	
	/**
	 * @return the number of rows in this matrix.
	 */
	private int getRowCount() {
		return this.my_current_matrix.size();
	}
	
	/**
	 * Creates a vector using the values contained in the ith column of the matrix.
	 * 
	 * @param i the column in question.
	 * @return a vector containing all of the values in the column.
	 */
	public Vector getColumnVector(int i) {
		ArrayList<Double> column = new ArrayList<Double>();
		for(Vector v : this.my_current_matrix) {
			column.add(v.getValue(i));
		}
		return new Vector(column);
	}
	
	/**
	 * Returns the row vector for the row specified.
	 * 
	 * @param i the row to examine.
	 * @return the vector.
	 */
	public Vector getRowVector(int i) {
		return this.my_current_matrix.get(i);
	}
	
	/**
	 * Initialize the ID matrix for a 4x4 matrix.
	 * 
	 * @return the constructed matrix.
	 */
	private List<Vector> buildIDMatrix() {
		List<Vector> theList = new ArrayList<Vector>();
		theList.add(new Vector(1, 0, 0, 0));
		theList.add(new Vector(0, 1, 0, 0));
		theList.add(new Vector(0, 0, 1, 0));
		theList.add(new Vector(0, 0, 0, 1));
		return theList;
	}
	
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		for(Vector v : this.my_current_matrix) {
			sb.append(v.toString());
			sb.append("\n");
		}
		return sb.toString();
	}
	
}
